Importar datos a R

Una vez descargados los archivos de datos, vamos a ubicarlos al interior de una carpeta llamada data que a su vez crearemos dentro del directorio de nuestro proyecto.

Paquetes para importar datos

# Instalamos los paquetes
install.packages("tidyverse", dependencies=TRUE, INSTALL_opts = c("--no-multiarch"))
install.packages("readxl")
install.packages("haven")
install.packages("googlesheets4")
install.packages("DBI")
install.packages("RMySQL")

# Cargamos los paquetes
library("tidyverse")
library("readxl")
library("haven")
library("googlesheets4")
library("DBI")
library("RMySQL")

Archivos de texto plano (.txt)

read_delim(
  file = "data/Boston_Housing.txt",
  delim = "|", 
  locale=locale(decimal_mark = ",")
  ) -> boston_housing_txt

read.table(
  file =  "data/Boston_Housing.txt",
  sep =  "|",
  header = TRUE,
  dec = ",",
  fileEncoding = "UTF-8"
  ) -> boston_housing_txt

str(boston_housing_txt)

Archivo de valores separados por comas (.csv)

read_csv2(
  file = "data/Boston_Housing.csv"
) -> boston_housing_csv

str(boston_housing_csv)

Archivos de Excel (.xls .xlsx)

## library("readxl")
read_excel(
  path = "data/Boston_Housing.xls"
) -> boston_housing_xls

str(boston_housing_xls)

read_xlsx(
  path = "data/Boston_Housing.xlsx", 
  sheet="Data"
) -> boston_housing_xlsx

str(boston_housing_xlsx)

Archivos de STATA (.dta)

## library("haven")
read_dta(
  file = "data/Boston_Housing.dta"
) -> boston_housing_dta

str(boston_housing_dta)

Archivos de bases de datos de SPSS

## library("haven")
read_sav(
  file = "data/PRICING.sav"
) -> pricing_sav

str(pricing_sav)

Hojas de cálculo en Google Sheets

Podemos leer desde R hojas de cálculo públicas o privadas.

  • Hojas públicas
# library("googlesheets4")
# Comando para que no nos exija autenticación
gs4_deauth()
# Declaro el enlace
link = "https://docs.google.com/spreadsheets/d/1z7uEedjNKXN4ub5lNZLO-dJhtWs4-Lv3PRcOTf-HWdk/edit?usp=sharing"
# Lectura de datos
boston_housing_gs <- read_sheet(link)

str(boston_housing_gs)
  • Hojas privadas
# library("googlesheets4")
# Comando para validar autenticación
gs4_auth()
# Declaro el enlace
link = "https://docs.google.com/spreadsheets/d/1z7uEedjNKXN4ub5lNZLO-dJhtWs4-Lv3PRcOTf-HWdk/edit?usp=sharing"
# Lectura de datos
boston_housing_gs <- read_sheet(link)

str(boston_housing_gs)

Usando la interfaz de RStudio

Ya sabemos importar datos.

Ahora vamos a ver distintos elementos que necesitamos para procesarlos.

Algoritmos

Un algoritmo es un conjunto finito de instrucciones que, si se siguen rigurosamente, llevan a cabo una tarea específica.

Todos los algoritmos se componen de “partes” básicas que se utilizan para crear “partes” más complejas.

El tratamiento, análisis y modelado de datos lo haremos mediante algoritmos.

Variables y datos

Tipos de variables y operaciones

Enfoque teórico

Enfoque desde la programación

   

Asignación

# Una forma de hacer asignación
objeto = "valor"
# Otra forma
objeto <- "valor"
# Ejemplos
pais = "Colombia"
departamentos = 32
tenemos_mar = TRUE

Tipos de variables

  • Booleanos (lógicos)
    • VERDADERO
    • FALSO
objeto_nombrado_por_mi <- TRUE # Siempre con mayúsculas
objeto_nombrado_por_mi
## [1] TRUE
class(objeto_nombrado_por_mi)
## [1] "logical"
is.logical(objeto_nombrado_por_mi)
## [1] TRUE
  • Numéricos
    • Enteros
    • Reales
pi
## [1] 3.141593
objeto_nombrado_por_mi <- 0
objeto_nombrado_por_mi
## [1] 0
class(objeto_nombrado_por_mi)
## [1] "numeric"
is.numeric(objeto_nombrado_por_mi)
## [1] TRUE
  • Alfanuméricos: caracteres o cadenas de texto
objeto_nombrado_por_mi <- "hola mundo"
objeto_nombrado_por_mi
## [1] "hola mundo"
class(objeto_nombrado_por_mi)
## [1] "character"
is.character(objeto_nombrado_por_mi)
## [1] TRUE
  • Fechas
objeto_nombrado_por_mi <- "2022-03-24" # Recomendado: ISO 8601 para fechas
objeto_nombrado_por_mi
## [1] "2022-03-24"
class(objeto_nombrado_por_mi)
## [1] "character"
otro_objeto_distinto <- as.Date(objeto_nombrado_por_mi)
class(otro_objeto_distinto)
## [1] "Date"
  • NA
objeto_nombrado_por_mi <- NA # Siempre en mayúsculas NA
objeto_nombrado_por_mi
## [1] NA
class(objeto_nombrado_por_mi)
## [1] "logical"

Método is

cualquier_cosa <- TRUE
is.logical(cualquier_cosa)
## [1] TRUE
is.numeric(cualquier_cosa)
## [1] FALSE
is.character(cualquier_cosa)
## [1] FALSE

Método as

TRUE -> true_logico
true_logico
## [1] TRUE
class(true_logico)
## [1] "logical"
as.character(true_logico) -> true_char
true_char
## [1] "TRUE"
class(true_char)
## [1] "character"
1 -> uno_numeric
uno_numeric
## [1] 1
class(uno_numeric)
## [1] "numeric"
as.character(uno_numeric) -> uno_char
uno_char
## [1] "1"
class(uno_char)
## [1] "character"

Conversiones

Desde Hacia
logical numeric
logical character
numeric character
numeric Date
character Date

Operadores matemáticos

2+2
## [1] 4
5-2
## [1] 3
3*4
## [1] 12
5/4
## [1] 1.25
9 %% 2
## [1] 1
3 ** 3
## [1] 27
3 ^ 3
## [1] 27
log(10)
## [1] 2.302585
sqrt(16)
## [1] 4

Operadores para comparación

5 > 2
## [1] TRUE
5 < 2
## [1] FALSE
10 == 10
## [1] TRUE
10 == 9
## [1] FALSE
10 != 9
## [1] TRUE
10 >= 10
## [1] TRUE
10 <= 8
## [1] FALSE

Operadores lógicos

  • Conjunción (se cumplen ambas): &&
  • Disyunción (se cumple alguna): ||
  • Negación (lo contrario): !

Orden de las operaciones

  • PEMDAS
    • Paréntesis
    • Exponentes
    • Multiplicaciones / Divisiones
    • Adición / Sustracción

Variables

pi = 3.1415
radio = 3
area = pi * radio**2
area
## [1] 28.2735
round(area, 2)
## [1] 28.27

Vectores, matrices, arreglos, listas y tablas

Un tipo especial de variables: factores

Son vectores numéricos enmascarados como caracteres. Se usan para crear grupos usando clasificaciones o codificaciones de las variables de interés. Estos factores pueden o no tener un orden.

Ejemplos: estrato socioeconómico, nivel de estudios, mes, sexo, localidad.

Vectores

1:5
## [1] 1 2 3 4 5
letters
##  [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s"
## [20] "t" "u" "v" "w" "x" "y" "z"
LETTERS
##  [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
## [20] "T" "U" "V" "W" "X" "Y" "Z"
c(1, 3, 2, 15, 4, 0, 0, 0, 1)
## [1]  1  3  2 15  4  0  0  0  1
seq(10, 100)
##  [1]  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28
## [20]  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47
## [39]  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66
## [58]  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85
## [77]  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100
seq(10, 100, by = 5)
##  [1]  10  15  20  25  30  35  40  45  50  55  60  65  70  75  80  85  90  95 100
seq(10, 100, length.out = 8)
## [1]  10.00000  22.85714  35.71429  48.57143  61.42857  74.28571  87.14286
## [8] 100.00000

Factores

as.factor(letters)
##  [1] a b c d e f g h i j k l m n o p q r s t u v w x y z
## Levels: a b c d e f g h i j k l m n o p q r s t u v w x y z

Funciones sobre vectores

vector_logico <- c(TRUE,FALSE,FALSE,TRUE,FALSE)
vector_cualquiera <- seq(1, 100, by = 3)
un_vector <- c(1, 2, 3, 4, 5)
otro_vector <- c(6, 7, 8, 9, 10)
which(vector_logico) # me dice cuales son los verdaderos 
## [1] 1 4
length(vector_cualquiera) # me dice cuánto mide el vector
## [1] 34
c(un_vector, otro_vector) # concatena los vectores
##  [1]  1  2  3  4  5  6  7  8  9 10

Operaciones entre vectores

vector_numerico <- c(2, 4, 6, 8, 10)
vector_numeric_1 <- 1:3
vector_numeric_2 <- 3:5
vector_numerico > 3
## [1] FALSE  TRUE  TRUE  TRUE  TRUE
1:5 %in% 3:8
## [1] FALSE FALSE  TRUE  TRUE  TRUE
outer(vector_numeric_1, vector_numeric_2, "*")
##      [,1] [,2] [,3]
## [1,]    3    4    5
## [2,]    6    8   10
## [3,]    9   12   15
outer(vector_numeric_1, vector_numeric_2, ">")
##       [,1]  [,2]  [,3]
## [1,] FALSE FALSE FALSE
## [2,] FALSE FALSE FALSE
## [3,] FALSE FALSE FALSE

Operaciones entre vectores (conjuntos)

union(vector_numeric_1, vector_numeric_2)
## [1] 1 2 3 4 5
intersect(vector_numeric_1, vector_numeric_2)
## [1] 3
setdiff(vector_numeric_1, vector_numeric_2)
## [1] 1 2

Matrices

matrix(data = 1:12, nrow = 3)
##      [,1] [,2] [,3] [,4]
## [1,]    1    4    7   10
## [2,]    2    5    8   11
## [3,]    3    6    9   12
matrix(data = 1:12, nrow = 6)
##      [,1] [,2]
## [1,]    1    7
## [2,]    2    8
## [3,]    3    9
## [4,]    4   10
## [5,]    5   11
## [6,]    6   12
matrix(data = 1:12, ncol = 6)
##      [,1] [,2] [,3] [,4] [,5] [,6]
## [1,]    1    3    5    7    9   11
## [2,]    2    4    6    8   10   12
matrix(data = 1:12, nrow = 4)
##      [,1] [,2] [,3]
## [1,]    1    5    9
## [2,]    2    6   10
## [3,]    3    7   11
## [4,]    4    8   12
matrix(data = 1:12, nrow = 4, byrow = TRUE)
##      [,1] [,2] [,3]
## [1,]    1    2    3
## [2,]    4    5    6
## [3,]    7    8    9
## [4,]   10   11   12
matrix(data = seq(0, 9, length.out = 4), nrow = 2) -> mi_matriz
mi_matriz
##      [,1] [,2]
## [1,]    0    6
## [2,]    3    9

Operaciones sobre matrices

otra_matriz # Toca inventársela
mi_matriz*2
mi_matriz + otra_matriz
mi_matriz*otra_matriz # Producto celda por celda
mi_matriz %*% otra_matriz # Producto de matrices

Funciones sobre matrices

t(mi_matriz) #mi_matriz transpuesta
##      [,1] [,2]
## [1,]    0    3
## [2,]    6    9
diag(mi_matriz) #Diagonal de mi_matriz
## [1] 0 9
det(mi_matriz) # Determinante, debe dar un número
## [1] -18
solve(mi_matriz) # Matriz inversa, sólo se puede con matrices cuadradas de determinante distinto de cero
##            [,1]      [,2]
## [1,] -0.5000000 0.3333333
## [2,]  0.1666667 0.0000000
dim(mi_matriz) # Dimensión de mi matriz
## [1] 2 2

Bibliografía complementaria: Capítulo 2: Linear Algebra, del libro Deep Learning del MIT

Tablas

iris
diamonds
?diamonds
mpg
?mpg

class(diamonds)
class(mpg)

str(diamonds)
str(mpg)

View(diamonds)
View(mpg)

Extracción [.

cuales_extraer <- c(1, 8, 6, 3)
letters[cuales_extraer] # Extrae las letras 1, 8, 6, 3
## [1] "a" "h" "f" "c"
vector_numerico[vector_numerico > 3] # Extrae los valores mayores a 3
## [1]  4  6  8 10
un_vector[1] # Extrae el elemento #1 del vector  
## [1] 1
mi_matriz[1,2] # Extrae el valor en la fila 1 columna 2
## [1] 6
mi_matriz[,1] # Extrae la primera columna
## [1] 0 3
mi_matriz[2,] # Extrae la segunda fila
## [1] 3 9
diamonds[,8] # Extrae la fila 8
## # A tibble: 53,940 × 1
##        x
##    <dbl>
##  1  3.95
##  2  3.89
##  3  4.05
##  4  4.2 
##  5  4.34
##  6  3.94
##  7  3.95
##  8  4.07
##  9  3.87
## 10  4   
## # ℹ 53,930 more rows
diamonds["x"] # Extrae la fila 8 con el nombre de la variable
## # A tibble: 53,940 × 1
##        x
##    <dbl>
##  1  3.95
##  2  3.89
##  3  4.05
##  4  4.2 
##  5  4.34
##  6  3.94
##  7  3.95
##  8  4.07
##  9  3.87
## 10  4   
## # ℹ 53,930 more rows
cuales_extraer = c("x","y","z") # Creo un vector de variables a extraer
diamonds[cuales_extraer] #Hago la extracción
## # A tibble: 53,940 × 3
##        x     y     z
##    <dbl> <dbl> <dbl>
##  1  3.95  3.98  2.43
##  2  3.89  3.84  2.31
##  3  4.05  4.07  2.31
##  4  4.2   4.23  2.63
##  5  4.34  4.35  2.75
##  6  3.94  3.96  2.48
##  7  3.95  3.98  2.47
##  8  4.07  4.11  2.53
##  9  3.87  3.78  2.49
## 10  4     4.05  2.39
## # ℹ 53,930 more rows

Ejemplo

A partir de la base de datos de Boston, hagamos una prueba de hipótesis para testear si el valor medio de la vivienda (variable MEDV) está influenciado/afectado por el hecho que la vivienda limite con el río Charles River (variable dummy CHAS).

t.test(MEDV ~ CHAS, data = boston_housing_xlsx) -> t_test_precio_rio
t_test_precio_rio
## 
##  Welch Two Sample t-test
## 
## data:  MEDV by CHAS
## t = -3.1133, df = 36.876, p-value = 0.003567
## alternative hypothesis: true difference in means between group 0 and group 1 is not equal to 0
## 95 percent confidence interval:
##  -10.476831  -2.215483
## sample estimates:
## mean in group 0 mean in group 1 
##        22.09384        28.44000

¿Qué podríamos extraer de este objeto?

str(t_test_precio_rio)
## List of 10
##  $ statistic  : Named num -3.11
##   ..- attr(*, "names")= chr "t"
##  $ parameter  : Named num 36.9
##   ..- attr(*, "names")= chr "df"
##  $ p.value    : num 0.00357
##  $ conf.int   : num [1:2] -10.48 -2.22
##   ..- attr(*, "conf.level")= num 0.95
##  $ estimate   : Named num [1:2] 22.1 28.4
##   ..- attr(*, "names")= chr [1:2] "mean in group 0" "mean in group 1"
##  $ null.value : Named num 0
##   ..- attr(*, "names")= chr "difference in means between group 0 and group 1"
##  $ stderr     : num 2.04
##  $ alternative: chr "two.sided"
##  $ method     : chr "Welch Two Sample t-test"
##  $ data.name  : chr "MEDV by CHAS"
##  - attr(*, "class")= chr "htest"

Extraigamos, por ejemplo, el p-valor de la prueba.

t_test_precio_rio["p.value"]
## $p.value
## [1] 0.00356717
t_test_precio_rio[["p.value"]]
## [1] 0.00356717
# Otra forma
t_test_precio_rio$p.value
## [1] 0.00356717

Podemos extraer partes de todos los objetos que tengamos en nuestro ambiente de trabajo.

Otro ejemplo

Ajustemos un modelo de regresión lineal simple usando como variable respuesta el valor medio de la vivienda (variable MEDV) en función del número medio de habitaciones de la vivienda (variable RM).

lm(MEDV ~ RM, data = boston_housing_xlsx) -> modelo_precio_habitaciones
modelo_precio_habitaciones
## 
## Call:
## lm(formula = MEDV ~ RM, data = boston_housing_xlsx)
## 
## Coefficients:
## (Intercept)           RM  
##     -34.671        9.102

summary(modelo_precio_habitaciones)
## 
## Call:
## lm(formula = MEDV ~ RM, data = boston_housing_xlsx)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -23.346  -2.547   0.090   2.986  39.433 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -34.671      2.650  -13.08   <2e-16 ***
## RM             9.102      0.419   21.72   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 6.616 on 504 degrees of freedom
## Multiple R-squared:  0.4835, Adjusted R-squared:  0.4825 
## F-statistic: 471.8 on 1 and 504 DF,  p-value: < 2.2e-16

¿Qué podríamos extraer de este objeto?

str(summary(modelo_precio_habitaciones))
## List of 11
##  $ call         : language lm(formula = MEDV ~ RM, data = boston_housing_xlsx)
##  $ terms        :Classes 'terms', 'formula'  language MEDV ~ RM
##   .. ..- attr(*, "variables")= language list(MEDV, RM)
##   .. ..- attr(*, "factors")= int [1:2, 1] 0 1
##   .. .. ..- attr(*, "dimnames")=List of 2
##   .. .. .. ..$ : chr [1:2] "MEDV" "RM"
##   .. .. .. ..$ : chr "RM"
##   .. ..- attr(*, "term.labels")= chr "RM"
##   .. ..- attr(*, "order")= int 1
##   .. ..- attr(*, "intercept")= int 1
##   .. ..- attr(*, "response")= int 1
##   .. ..- attr(*, ".Environment")=<environment: R_GlobalEnv> 
##   .. ..- attr(*, "predvars")= language list(MEDV, RM)
##   .. ..- attr(*, "dataClasses")= Named chr [1:2] "numeric" "numeric"
##   .. .. ..- attr(*, "names")= chr [1:2] "MEDV" "RM"
##  $ residuals    : Named num [1:506] -1.18 -2.17 3.97 4.37 5.82 ...
##   ..- attr(*, "names")= chr [1:506] "1" "2" "3" "4" ...
##  $ coefficients : num [1:2, 1:4] -34.671 9.102 2.65 0.419 -13.084 ...
##   ..- attr(*, "dimnames")=List of 2
##   .. ..$ : chr [1:2] "(Intercept)" "RM"
##   .. ..$ : chr [1:4] "Estimate" "Std. Error" "t value" "Pr(>|t|)"
##  $ aliased      : Named logi [1:2] FALSE FALSE
##   ..- attr(*, "names")= chr [1:2] "(Intercept)" "RM"
##  $ sigma        : num 6.62
##  $ df           : int [1:3] 2 504 2
##  $ r.squared    : num 0.484
##  $ adj.r.squared: num 0.483
##  $ fstatistic   : Named num [1:3] 472 1 504
##   ..- attr(*, "names")= chr [1:3] "value" "numdf" "dendf"
##  $ cov.unscaled : num [1:2, 1:2] 0.1604 -0.02521 -0.02521 0.00401
##   ..- attr(*, "dimnames")=List of 2
##   .. ..$ : chr [1:2] "(Intercept)" "RM"
##   .. ..$ : chr [1:2] "(Intercept)" "RM"
##  - attr(*, "class")= chr "summary.lm"

Extraigamos el \(R^2\) ajustado del modelo.

summary(modelo_precio_habitaciones)$adj.r.squared
## [1] 0.4825007

Control flow

El control flow es un conjunto de funciones que permiten manejar las órdenes de manera estructurada y lógica. Las más importantes son:

?Control

Loops

Todos los lenguajes modernos de programación ofrecen una o más maneras de realizar operaciones iterativas. El poder repetir la misma acción una cantidad indefinida de veces es una de las grandes ventajas de realizar las tareas mediante programación.

for

Sirve para crear tareas repetitivas de un número de pasos específico.

Uno de los usos más frecuentes de un ciclo for es la configuración de métodos de remuestreo (bootstraping).

#vamos a guardar en una lista los coeficientes de una regresión
coeficientes <- list()

# inicializo el ciclo for
for(i in 1:1000){
  #en cada paso
  
  # 1. saco una muestra de 100 casas
  muestra <- sample_n(boston_housing_xlsx, 100)
  
  # 2. ajusto un modelo de regresión lineal
  lm(MEDV ~ RM, data = muestra) -> modelo
  
  # 3. extraigo y almaceno los coeficientes del modelo
  coeficientes[[i]] <- coefficients(modelo)
}

# grafico el comportamiento de los coeficientes
coeficientes %>% 
  transpose %>% 
  lapply(unlist) %>% 
  as_tibble() %>% 
  gather(key = coeficiente, value = valor) %>% 
  ggplot +
  aes(x = valor) + 
  geom_density() +
  facet_wrap(~coeficiente, nrow = 2,  scales = "free")

while

Sirve para crear tareas repetitivas que no sabemos después de cuántos pasos terminan. Requiere una inicialización cuidadosa.

Ejemplo: ¿Cuántos sobres tengo que comprar para llenar un álbum de 100 cromos?

# inicializo las condiciones de partida
album <- iteracion <- 0
# creo la condición lógica que permite ejecutar el proceso
aun_falta <- TRUE

# siempre que aun_falta siga siendo verdadero
while(aun_falta){
  # en cada ciclo
  
  # 1. actualizo en qué iteración voy
  iteracion <- iteracion + 1
  
  # 2. extraigo una muestra de 6 números ("compro un sobre con 6 cromos")
  sobre <- sample(100, 6)
  
  # 3.1 tomo el álbum
  # 3.2 le combino los cromos que obtuve
  # 3.3 ordeno los cromos de menor a mayor
  # 3.4 dejo valores únicos (quito cromos duplicados)
  # 3.5 actualizo el álbum
  album %>% c(sobre) %>% sort %>% unique -> album
  
  # 4. si tengo menos de 100 cromos es porque me falta
  length(album) < 100 -> aun_falta
}

# muestro el número de iteraciones
# es decir, cuántos sobres tuve que comprar
iteracion
## [1] 82

if, else

La estructura if sirve para ejecutar varias rutinas distintas dependiendo de una condición lógica. En caso de que sea necesario, es posible aplicar una rutina alterna con la estructura else.

Ejemplo: Prueba de normalidad.

Diversas pruebas y modelos estadísticos requieren verificar el supuesto de normalidad en los datos.

# de la base de datos de carros
# extraigo la variable mpg
# le hago un test de shapiro
# guardo los resultados de la prueba en un objeto llamado prueba_sw
mtcars[["mpg"]] %>% shapiro.test() -> prueba_sw

# estructura condicional
if(prueba_sw$p.value > 0.05){
  # Si acepto la hipótesis de normalidad en la variable mpg
  # Hago una prueba t
  print("La variable mpg sigue una distribución normal")
  print("Realizo una prueba t")
  t.test(mpg ~ vs, data = mtcars)
} else {
  # Si rechazo la hipótesis de normalidad en la variable mpg
  # Hago una prueba Mann-Whitney-Wilcoxon
  print("La variable mpg no sigue una distribución normal")
  print("Realizo una prueba Mann-Whitney-Wilcoxon")
  wilcox.test(mpg ~ vs, data = mtcars)
}
## [1] "La variable mpg sigue una distribución normal"
## [1] "Realizo una prueba t"
## 
##  Welch Two Sample t-test
## 
## data:  mpg by vs
## t = -4.6671, df = 22.716, p-value = 0.0001098
## alternative hypothesis: true difference in means between group 0 and group 1 is not equal to 0
## 95 percent confidence interval:
##  -11.462508  -4.418445
## sample estimates:
## mean in group 0 mean in group 1 
##        16.61667        24.55714